/*
 * Copyright 2011 J. Auer, J. Andert, F.-J. Jendrusch, U. Celebi
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fuberlin.replication.communication.wifi.android;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Vector;

import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.net.wifi.WifiManager.WifiLock;
import android.util.Log;
import de.fuberlin.replication.communication.Communication;
import de.fuberlin.replication.communication.CommunicationNotYetEstablishedException;

/**
 * Support for sending/receiving data and discovery of remote devices in an wireless network, using TCP/IP.
 */
public class AndroidWiFiCommunication implements Communication {

	private static final String TAG = "WiFiCom";
	private static final String MULTICASTADDRESS = "230.3.20.11";
	private static final int PORT = 31214;

	private WifiLock lock;
	private Socket socket;
	private ObjectInputStream inputStream;
	private ObjectOutputStream outputStream;
	private State state = State.IDLE;

	private enum State {
		IDLE, LISTENING, CONNECTING, ESTABLISHED
	}

	@Override
	public Object read() throws IOException {
		if (state != State.ESTABLISHED)
			throw new CommunicationNotYetEstablishedException("Communication not opened");

		try {
			return inputStream.readObject();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		return null;
	}

	@Override
	public void write(Object object) throws IOException {
		if (state != State.ESTABLISHED)
			throw new CommunicationNotYetEstablishedException("Communication not opened");

		outputStream.writeObject(object);
		outputStream.flush();
	}

	private synchronized void setState(State state) {
		this.state = state;
		notifyAll();
	}

	private synchronized void open() throws IOException {
		Log.d(TAG, "connected");
		outputStream = new ObjectOutputStream(socket.getOutputStream());
		inputStream = new ObjectInputStream(socket.getInputStream());
		setState(State.ESTABLISHED);
	}

	/**
	 * Actively connect to a remote device. The remote device must have called <code>listen</code> before.
	 * 
	 * @param host Remote host to connect to
	 * @param context Context of this device for acquiring WiFi locks
	 * @see #listen(Context)
	 */
	public synchronized void connect(String host, Context context) {
		setState(State.CONNECTING);
		Log.d(TAG, "acquire wifilock");
		lock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)).createWifiLock(
				WifiManager.WIFI_MODE_FULL, "replicationLock");
		lock.acquire();
		long overallTimeout = System.currentTimeMillis() + 30000;
		Log.d(TAG, "connect to: " + host);

		while (state == State.CONNECTING) {
			if (System.currentTimeMillis() > overallTimeout)
				throw new CommunicationNotYetEstablishedException();

			try {
				socket = new Socket(InetAddress.getByName(host), PORT);
				open();
			} catch (IOException e) {
				Log.w(TAG, e);
				try {
					wait(1000);
				} catch (InterruptedException e1) {
				}
			}
		}
	}

	/**
	 * Wait for incoming connections.
	 * 
	 * @param context Context of this device for acquiring WiFi locks
	 * @throws IOException Thrown, if some Exception occurs at the underlying socket
	 * @see #connect(String, Context)
	 */
	public synchronized void listen(Context context) throws IOException {
		setState(State.LISTENING);
		Log.d(TAG, "acquire wifilock");
		lock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)).createWifiLock(
				WifiManager.WIFI_MODE_FULL, "replicationLock");
		lock.acquire();
		ServerSocket s = new ServerSocket(PORT);
		s.setSoTimeout(1000);
		Log.d(TAG, "listening");

		try {
			while (state == State.LISTENING)
				try {
					socket = s.accept();
					open();
				} catch (SocketTimeoutException e) {
					try {
						wait(500);
					} catch (InterruptedException e1) {
					}
				}
		} finally {
			s.close();
		}
	}

	/**
	 * Auto decide socket direction.
	 * 
	 * @param localHost Local IP address
	 * @param remoteHost Remote IP address
	 * @param context Base Context of this device for acquiring WiFi locks
	 * @throws IOException Thrown, if some Exception occurs at the underlying socket
	 */
	public void autoConnect(String localHost, String remoteHost, Context context) throws IOException {
		if (localHost.compareTo(remoteHost) < 0)
			listen(context);
		else
			connect(remoteHost, context);
	}

	/**
	 * Close the communication socket and release the WiFi lock.
	 */
	public synchronized void close() {
		setState(State.IDLE);

		try {
			inputStream.close();
			outputStream.close();
			socket.close();
		} catch (NullPointerException e1) {
		} catch (IOException e2) {
		}

		try {
			lock.release();
		} catch (RuntimeException e) {
		}
		Log.d(TAG, "communication closed");
	}

	/**
	 * Auxiliary class for broadcasting "hello-packages" for a given time.
	 */
	protected static class Announcer implements Runnable {

		private MulticastSocket mcSocket;
		private DatagramPacket packet;
		private int iterations;

		/**
		 * Constructor.
		 * 
		 * @param socket MulticastSocket to send packages over
		 * @param timeout Time in ms. The class will broadcast for timeout ms.
		 * @throws UnknownHostException Thrown, if MulticastGroup is not known anymore
		 */
		public Announcer(MulticastSocket socket, int timeout) throws UnknownHostException {
			this.mcSocket = socket;
			packet = new DatagramPacket("sync?".getBytes(), 5, InetAddress.getByName(MULTICASTADDRESS), PORT);
			iterations = timeout / 1000;
		}

		@Override
		public void run() {
			for (int i = 0; i < iterations; ++i) {
				try {
					mcSocket.send(packet);
				} catch (IOException e) {
					Log.v("Exception", "Unable to send!");
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					Log.v("Exception", "Unable to send!");
				}
			}
		}
	}

	/**
	 * Discover devices in the local network, this device is connected to, that are discovering, too. May fail, if the
	 * network doesn't support broadcasts or the device doesn't allow joining MulticastGroups.
	 * 
	 * @param timeout Time to search, in ms.
	 * @param context Base Context for acquiring WiFi locks
	 * @return Array of strings, representing the IP addresses of discovered devices
	 * @throws IOException Thrown, of some error occurs on the underlying socket
	 */
	public static String[] discoverDevices(int timeout, Context context) throws IOException {
		// Acquire locks
		Log.d(TAG, "acquire wifilock");
		WifiLock wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)).createWifiLock(
				WifiManager.WIFI_MODE_FULL, "replicationLock");
		wifiLock.acquire();
		Log.d(TAG, "acquire multicastlock");
		MulticastLock mcLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
				.createMulticastLock("replicationMulticastLock");
		mcLock.acquire();

		// Start a thread, that sends out "hello-packages"
		Log.d(TAG, "collecting device data");
		MulticastSocket socket = new MulticastSocket(PORT);
		socket.setTimeToLive(2);
		socket.joinGroup(InetAddress.getByName(MULTICASTADDRESS));
		new Thread(new Announcer(socket, timeout)).start();

		// Receive incoming packages for a given time, and collect the remote addresses in a list
		Vector<String> devices = new Vector<String>();
		long haltTime = System.currentTimeMillis() + timeout;

		while (System.currentTimeMillis() < haltTime) {
			DatagramPacket d = new DatagramPacket(new byte[5], 5);
			socket.receive(d);
			String remoteHost = d.getAddress().getHostName();

			if (new String(d.getData()).equals("sync?") && !devices.contains(remoteHost)) {
				devices.add(remoteHost);
				Log.d(TAG, "device found: " + remoteHost);
			}
		}

		socket.close();
		mcLock.release();
		wifiLock.release();
		String[] result = new String[devices.size()];
		devices.toArray(result);

		return result;
	}

	/**
	 * Causes the current thread to wait until the communication is established, or some other thread interrupts the
	 * current thread, or a certain amount of real time has elapsed.
	 * 
	 * @param timeout the maximum time to wait in milliseconds.
	 * @throws InterruptedException if any thread interrupted the current thread before or while the current thread was
	 *             waiting for the communication to be established. The interrupted status of the current thread is
	 *             cleared when this exception is thrown.
	 * @throws CommunicationNotYetEstablishedException if the communication is not yet established after waiting.
	 * @see Object#wait(long)
	 */
	public synchronized void waitEstablished(long timeout) throws InterruptedException {
		if (state == State.CONNECTING || state == State.LISTENING)
			wait(timeout);

		if (state != State.ESTABLISHED)
			throw new CommunicationNotYetEstablishedException();
	}
}